(ns scripts.cljmon.query
  (:use scripts.cljmon.process
        util.mon
        util.utils
        clojure.tools.logging)
  (:require clojure.string))

(defn- ?b [bytes u]
  (quot bytes ({"B" 1
                "BYTES" 1
                "K" 1000
                "KB" 1000
                "KIB" 1024
                "M" (* 1000 1000)
                "MB" (* 1000 1000)
                "MIB" (* 1024 1024)
                "G" (* 1000 1000 1000)
                "GB" (* 1000 1000 1000)
                "GIB" (* 1024 1024 1024)} (.toUpperCase u))))

(defn- +b [field u]
  (format "%s (%s)" field u))

(defn- match-pid-by-arg [pid x]
  (when-let [args (exception-log (get-process-args pid))]
    (if (re? x)
      (some #(re-find x %) args)
      ((set args) x))))

(defn- find-pids [name multiple-instances filter-args]
  {:pre [(or (true? multiple-instances) (false? multiple-instances) (nil? multiple-instances))]}
  (let [pids (if (re-matches #"\d+" name) [(Integer/parseInt name)] (find-all-processes name))
        pids (if filter-args (filter #(match-pid-by-arg % filter-args) pids) pids)]
    (if multiple-instances
      pids
      [(as-one pids (str "process " name))])))

(defn- translate-process-query [{:keys [by pid name port multiple-instances filter-args] :as q}]
  (case by
    :pid q
    :name (if (= multiple-instances :acc)
            (assoc q :dynamic true)
            (for [pid (find-pids name multiple-instances filter-args)]
              (assoc q :pid pid)))
    :port (assoc q :pid (find-pid-by-tcp-port port))
    :me (assoc q :pid (get-pid))
    nil (throw (Exception. ":by must by set for a process-info query"))))

(defn- translate-local-query [q]
  (let [q (if (keyword? q) {:type q} q)
        t (:type q)]
    (cond
      (= t :timestamp) q
      (= t :system-info) q
      (= t :wait-cpu) q
      (= t :disk-io) q
      (= t :disk-space) (for [disk (get-disk-list)] {:type :disk-space :disk disk})
      (= t :network-io) q
      (= t :process-info) (translate-process-query q)
      (= t :fn) q
      :else (throw (Exception. (str "Invalid query: " q))))))

(defn translate-local-queries [queries]
  ; TODO: refactor check using a function to improve error message
  {:pre [(->> queries (filter #(= (:type %) :process-info)) (keep :label) (cons :holder) (apply distinct?))
         (->> queries (filter #(= (:type %) :process-info)) (every? #(or (not (:label %)) (not (true? (:multiple-instances %))))))]}
  (loop [qs (flatten (map translate-local-query queries))
         results []
         pids #{}]
    (if (empty? qs)
      results
      (let [q (first qs)]
        (if (and (= (:type q) :process-info) (pids (:pid q)))
          (recur (next qs) results pids)
          (recur (next qs) (conj results q) (conj pids (:pid q))))))))

(defn- collect-local-comments-for [{:keys [type pid label log-process-arguments dynamic]}]
  (when (and (= type :process-info) (not dynamic))
    (let [info (assoc (get-process-info pid) :label label)
          ks (remove nil? [:pid :name (when label :label) :bin (when log-process-arguments :args) :cwd])]
      (clojure.string/join ", "
        (for [k ks] (str (name k) ": " (get info k)))))))

(defn collect-local-comments [queries]
  (->> queries
    (map collect-local-comments-for)
    flatten
    (remove nil?)
    doall))

(defn- collect-local-headers-for [{:keys [type label disk pid unit label]}]
  (case type
    :timestamp [{:label "Timestamp"}
                {:label "Date"}
                {:label "Time"}]
    :system-info [{:label "System CPU"}
                  {:label "System Memory Used" :unit unit}
                  {:label "System Memory Free" :unit unit}]
    :wait-cpu {:label "Wait CPU"}
    :disk-io {:label "Disk IO" :unit unit}
    :disk-space [{:label (format "File System %s Used" disk) :unit unit}
                 {:label (format "File System %s Free" disk) :unit unit}]
    :network-io {:label "Network IO" :unit unit}
    :process-info [{:label (str (or label pid) " CPU")}
                   {:label (str (or label pid) " VM") :unit unit}
                   {:label (str (or label pid) " Memory") :unit unit}]
    :fn {:label label :unit unit}))

(defn collect-local-headers [queries]
  (doall
    (for [{:keys [label unit]} (flatten (map collect-local-headers-for queries))]
      (if unit
        (+b label unit)
        label))))

(defn- collect-process-data-for-dynamic [{:keys [type name filter-args unit label] :as q}]
  {:pre [label]}
  (let [pids (find-pids name true filter-args)
        cpus (map get-process-cpu pids)
        mems (map get-process-memory pids)]
    [{:type type :column :cpu :label (str label " CPU") :value (* 100 (reduce + cpus))}
     {:type type :column :vm :label (str label " VM") :value (reduce + (map :vm mems)) :unit unit}
     {:type type :column :memory :label (str label " Memory") :value (reduce + (map :memory mems)) :unit unit}]))

(defn- collect-process-data-for [{:keys [type pid unit label] :as q}]
  (try
    (let [cpu (get-process-cpu pid)
          mem (get-process-memory pid)]
      [{:type type :column :cpu :label (str (or label pid) " CPU") :value (* 100 cpu)}
       {:type type :column :vm :label (str (or label pid) " VM") :value (:vm mem) :unit unit}
       {:type type :column :memory :label (str (or label pid) " Memory") :value (:memory mem) :unit unit}])
    (catch Exception e
      (error (format "Process %d status error, probably killed: %s" pid e))

      [{:type type :column :cpu :label (str (or label pid) " CPU") :value "-"}
       {:type type :column :vm :label (str (or label pid) " VM") :value "-"}
       {:type type :column :memory :label (str (or label pid) " Memory") :value "-"}])))

(defn- collect-local-data-for [{:keys [type disk unit label] :as q}]
  (case type
    :timestamp (let [now (now)
                     timestamp (.getTime now)
                     date (normalize-date now)
                     time (normalize-time now)]
                 [{:type type :column :timestamp :label "Timestamp" :value timestamp}
                  {:type type :column :date :label "Date" :value date}
                  {:type type :column :time :label "Time" :value time}])
    :system-info [{:type type :column :cpu :label "System CPU" :value (* 100 (get-system-cpu))}
                  {:type type :column :memory-used :label "System Memory Used" :value (get-system-memory-used) :unit unit}
                  {:type type :column :memory-free :label "System Memory Free" :value (get-system-memory-free) :unit unit}]
    :wait-cpu {:type type :label "Wait CPU" :value (* 100 (get-wait-cpu))}
    :disk-io {:type type :label "Disk IO" :value (get-disk-io) :unit unit}
    :disk-space (let [{:keys [used free]} (get-disk-space disk)]
                  [{:type type :column :used :label (format "File System %s Used" disk) :value used :unit unit}
                   {:type type :column :free :label (format "File System %s Free" disk) :value free :unit unit}])
    :network-io {:type type :label "Network IO" :value (get-network-io) :unit unit}
    :process-info (if (:dynamic q)
                    (collect-process-data-for-dynamic q)
                    (collect-process-data-for q))
    :fn {:type type :label (:label q) :value ((:fn q)) :unit unit}))

(defn collect-local-data [queries]
  (doall
    (for [{:keys [label value unit] :as x} (flatten (map collect-local-data-for queries))]
      (if unit
        (assoc x :label (+b label unit) :value (?b value unit))
        x))))

(defn- translate-queries [queries]
  (translate-local-queries queries))

(defn- collect-comments [queries]
  (collect-local-comments queries))

(defn- collect-headers [queries]
  (collect-local-headers queries))

(defn- collect-data [queries]
  (collect-local-data queries))

(defn create-query-session [{:keys [pdh? winapi?]}]
  ; TODO: XXX: if local is linux and remote is windows, pdh? and winapi? may raise an error?
  (in-new-monbox
    (with-if pdh?
      with-pdh-box
      (with-if pdh?
        (binding [get-process-memory pdh-get-process-memory])
        (with-if winapi?
          (binding [get-process-memory winapi-get-process-memory])
          (bound-fn*
            (fn [f]
              (f))))))))

(defmacro run-in-new-query-session [session-info & body]
  `(let [session# (create-query-session ~session-info)]
     (session#
       (fn []
         ~@body))))

;(def qs (translate-queries [:timestamp {:type :system-info :unit "MB"} {:type :process-info :by :name :name "java" :multiple-instances true :unit "MB"} {:type :fn :label "fn" :fn (constantly "hi")}]))
;(def qs (translate-queries [{:type :process-info :by :name :name "firefox" :unit "MB" :label "Firefox"}]))
;(def qs (translate-queries [{:type :process-info :by :name :name "java" :unit "MB" :label "Cljmon Agent" :filter-args "--agent"}]))
;(def qs (translate-queries [{:type :process-info :by :name :name "java" :unit "MB" :label "All Java" :multiple-instances :acc}]))
;(prn qs)
;(prn (collect-comments qs))
;(prn (collect-headers qs))
;(prn (collect-data qs))

